Skip to content

perf(css): audit :has() selectors and add stylelint guard for Safari#7708

Open
hectahertz wants to merge 4 commits intomainfrom
hectahertz/perf-css-has-safari-audit
Open

perf(css): audit :has() selectors and add stylelint guard for Safari#7708
hectahertz wants to merge 4 commits intomainfrom
hectahertz/perf-css-has-safari-audit

Conversation

@hectahertz
Copy link
Copy Markdown
Contributor

@hectahertz hectahertz commented Mar 27, 2026

Closes https://github.com/github/github-ui/issues/17224

In Aug 2025, a :has() selector on a broadly-present component caused 10-20+ second freezes on Safari due to quadratic style invalidation in WebKit (398 reactions, 86 participants, 446 points on HN). This PR audits all :has() usage in @primer/react and adds guardrails to prevent a repeat.

Audit summary: 46 :has() occurrences across 12 CSS Module files. All are low-risk because CSS Modules generate unique hashed class names, limiting the invalidation blast radius to a small subtree. The one dangerous pattern (body:has(...) in Dialog) was already mitigated with a feature flag guard.

Changes:

  1. Stylelint rule (selector-pseudo-class-disallowed-list: ['has']) blocks new :has() by default. Developers must add a stylelint-disable with justification.

  2. File-level stylelint-disable comments on all 12 audited files, referencing the tracking issue.

  3. Removed redundant :has() from @primer/styled-react BaseStyles. Same :has([data-color-mode]) pattern already removed from @primer/react in #7325. The global selectors already handle input color-scheme.

  4. :has() guidance in CSS instructions (.github/instructions/css.instructions.md) covering safe vs dangerous patterns.

Full audit with per-selector risk assessment posted on the tracking issue.

Changelog

New

  • Stylelint rule blocking :has() pseudo-class by default with Safari perf warning

Changed

  • Added stylelint-disable audit comments to 12 existing CSS files with :has() usage
  • CSS coding instructions now include :has() guidance

Removed

  • Removed expensive :has([data-color-mode]) selectors from @primer/styled-react BaseStyles (redundant with existing global selectors)

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

  • Verified stylelint catches new :has() usage with clear error message
  • Verified all existing files pass lint with stylelint-disable comments
  • Full npm run lint:css passes clean

Merge checklist

cc @mattcosta7

@hectahertz hectahertz requested a review from a team as a code owner March 27, 2026 17:25
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

🦋 Changeset detected

Latest commit: 5e93384

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@primer/react Patch
@primer/styled-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Mar 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Audits and hardens :has() selector usage across the Primer React monorepo to prevent Safari/WebKit performance regressions, adding lint guardrails and documenting safe usage patterns.

Changes:

  • Add a Stylelint rule to disallow :has() by default with a Safari performance warning message.
  • Add stylelint-disable audit comments to existing CSS Module files that already rely on :has().
  • Remove redundant :has([data-color-mode]) selectors from @primer/styled-react BaseStyles and document :has() guidance for contributors.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
stylelint.config.mjs Disallows :has() via Stylelint rule with error message guidance.
packages/styled-react/src/components/BaseStyles.tsx Removes :has([data-color-mode]) selectors from global BaseStyles styling.
packages/react/src/experimental/SelectPanel2/SelectPanel.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/TreeView/TreeView.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Timeline/Timeline.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/SegmentedControl/SegmentedControl.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/PageHeader/PageHeader.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Dialog/Dialog.module.css Adds file-level Stylelint disable for audited body:has(...) legacy path.
packages/react/src/ButtonGroup/ButtonGroup.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Button/ButtonBase.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Breadcrumbs/Breadcrumbs.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/AvatarStack/AvatarStack.module.css Extends existing disables to include the new :has() disallow rule.
packages/react/src/ActionList/Group.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/ActionList/ActionList.module.css Extends existing disables to include the new :has() disallow rule.
.github/instructions/css.instructions.md Documents :has() risks, safe patterns, and the required lint override approach.

*/
/* stylelint-disable-next-line selector-no-qualifying-type */
/* stylelint-disable-next-line selector-no-qualifying-type, selector-pseudo-class-disallowed-list -- :has() on body guarded by feature flag negation, audited (github/github-ui#17224) */
body:not([data-dialog-scroll-optimized]):has(.Dialog.DisableScroll) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can/should just ship the flag here that enables the optimization path soon!

I just keep missing the window's where it's unlocked to do that
#7633

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added #7633 to my list of “PRs to merge in the next release.” I’ll keep an eye on it and merge it the next time main is unlocked.

@hectahertz hectahertz force-pushed the hectahertz/perf-css-has-safari-audit branch from b025450 to b66b0cd Compare April 2, 2026 14:45
@primer
Copy link
Copy Markdown
Contributor

primer bot commented Apr 2, 2026

🤖 Formatting issues have been automatically fixed and committed to this PR.

@github-actions github-actions bot temporarily deployed to storybook-preview-7708 April 2, 2026 14:56 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants